package com.middlegroup.subjectpay.service.impl;

import com.alibaba.fastjson.JSON;
import com.middlegroup.common.enums.ConfigKeyEnums;
import com.middlegroup.common.enums.ServiceType;
import com.middlegroup.common.exception.ErrorMsg;
import com.middlegroup.common.model.PayRequest;
import com.middlegroup.common.model.TransResult;
import com.middlegroup.common.utils.DateUtils;
import com.middlegroup.subjectpay.entity.SubjectInBills;
import com.middlegroup.subjectpay.entity.SubjectInThirdBills;
import com.middlegroup.subjectpay.service.SubjectChannelConfigService;
import com.middlegroup.subjectpay.service.SubjectInBillsService;
import com.middlegroup.subjectpay.service.SubjectInThirdBillsService;
import com.middlegroup.subjectpay.service.WechatPayH5Service;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.annotation.Resource;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.*;

/**
 * Created by pc on 2019/1/14.
 */
@Service
@Slf4j
public class WechatPayH5ServiceImpl implements WechatPayH5Service {
    @Resource
    private SubjectInBillsService subjectInBillsService;
    @Resource
    private SubjectChannelConfigService subjectChannelConfigService;
    @Resource
    private RestTemplate restTemplate;
    @Resource
    private SubjectInThirdBillsService subjectInThirdBillsService;

   @Override
    public TransResult wechatPayH5(PayRequest request) {
        if (StringUtils.isBlank(request.getServiceType())){
            request.setServiceType(ServiceType.wx_h5.name());
        }

        TransResult transResult = new TransResult();
        transResult.setBillNo(request.getBillNo());
        transResult.setMerchantBillNo(request.getMerchantBillNo());
        transResult.setAmount(request.getAmount());
        transResult.setTransStatus(0);
        transResult.setTransTime(System.currentTimeMillis());

        SubjectInBills subjectInBills=new SubjectInBills();
        subjectInBills.setSubjectCode(request.getSubjectCode());
        subjectInBills.setMerchantBillNo(request.getMerchantBillNo());
        subjectInBills.setBillNo(request.getBillNo());
        subjectInBills.setChannelCode(request.getChannelCode().getCode());
        subjectInBills.setServiceType(request.getServiceType());
        subjectInBills.setAmount(request.getAmount());
        subjectInBills.setStatus(0);

        boolean rt = false;
        try {
            rt = subjectInBillsService.insert(subjectInBills);
        }catch (Exception e){
            log.error("插入主体端入款订单表(t_subject_in_bills)失败:"+request.getBillNo(),e);
        }

        if (!rt) {
            //插入失败
            transResult.setTransStatus(2);
            transResult.setMsg("插入主体端出款订单表(t_subject_in_bills)失败:"+request.getBillNo());
            return transResult;
        }

        //校验参数
        if (StringUtils.isBlank(request.getMerchantBillNo())){
            subjectInBills.setStatus(2);
            subjectInBillsService.updateById(subjectInBills);

            transResult.setTransStatus(2);
            transResult.setMsg(ErrorMsg.EMPTY_MERCHANTBILLNO);
            return transResult;
        }
        if (StringUtils.isBlank(request.getBillNo())){
            subjectInBills.setStatus(2);
            subjectInBillsService.updateById(subjectInBills);

            transResult.setTransStatus(2);
            transResult.setMsg(ErrorMsg.EMPTY_BILLNO);
            return transResult;
        }
        if (null == request.getChannelCode()){
            subjectInBills.setStatus(2);
            subjectInBillsService.updateById(subjectInBills);

            transResult.setTransStatus(2);
            transResult.setMsg(ErrorMsg.EMPTY_CHANNELCODE);
            return transResult;
        }
        if (StringUtils.isBlank(request.getSubjectCode())){
            subjectInBills.setStatus(2);
            subjectInBillsService.updateById(subjectInBills);

            transResult.setTransStatus(2);
            transResult.setMsg(ErrorMsg.EMPTY_SUBJECTCODE);
            return transResult;
        }

        /**
         * 此处一定要注意，调用第三方渠道接口必须返回TransResult，不可为空，且保证异常在接口中已处理
         */
        switch (request.getChannelCode()){
            case wx_h5:
                try{
                    //TODO 微信H5支付
                    transResult=this.unifiedorder(request);
                }catch (Exception e){
                    log.error("WECHANTPAYH5第三方通道微信H5支付接口调用失败"+request.getBillNo(),e);
                }
                break;
            default:
                transResult.setTransStatus(2);
                transResult.setMsg("未找到相应channelCode!");
        }

        //是否更新bills表状态
        boolean ifUpdateBillStatus = null != transResult && null != transResult.getTransStatus() && (transResult.getTransStatus().intValue()==1 || transResult.getTransStatus().intValue()==2);
        if (ifUpdateBillStatus){
            subjectInBills.setStatus(transResult.getTransStatus());
            subjectInBillsService.updateById(subjectInBills);
        }

        return transResult;
    }

    /**
     * 统一下单, H5支付(MWEB).
     * @return 支付跳转链接
     * <p> mweb_url为拉起微信支付收银台的中间页面，可通过访问该url来拉起微信客户端，完成支付,mweb_url的有效期为5分钟。
     */
    public TransResult unifiedorder(PayRequest request) {
        TransResult transResult = new TransResult();
        transResult.setBillNo(request.getBillNo());
        transResult.setMerchantBillNo(request.getMerchantBillNo());
        transResult.setAmount(request.getAmount());
        transResult.setTransStatus(0);
        transResult.setTransTime(System.currentTimeMillis());

        String category=request.getChannelCode().getCode();

        String appid=subjectChannelConfigService.getConfigString(category, ConfigKeyEnums.appid.getName());
        String mch_id=subjectChannelConfigService.getConfigString(category,ConfigKeyEnums.mch_id.getName());
        String mch_key=subjectChannelConfigService.getConfigString(category,ConfigKeyEnums.mch_key.getName());
        String notify_url=subjectChannelConfigService.getConfigString(category,ConfigKeyEnums.notify_url.getName());
        String wap_url=subjectChannelConfigService.getConfigString(category,ConfigKeyEnums.wap_url.getName());
        String requestUrl=subjectChannelConfigService.getConfigString(category,ConfigKeyEnums.url.getName());

        if(StringUtils.isBlank(requestUrl) || StringUtils.isBlank(appid) || StringUtils.isBlank(mch_id)
                || StringUtils.isBlank(mch_key)
                || StringUtils.isBlank(notify_url) || StringUtils.isBlank(wap_url)){
            transResult.setTransStatus(2);
            transResult.setMsg(ErrorMsg.INCOMPLETE_CONFIG_INFO);
            return transResult;
        }

        Map<String, String> paraMap = new HashMap<String, String>();
        paraMap.put("appid",appid);//公众账号ID.
        paraMap.put("mch_id",mch_id);//商户号.
        paraMap.put("nonce_str", this.generateNonceStr());//随机字符串.
        paraMap.put("body",request.getSubject());//商品描述.
        paraMap.put("out_trade_no",request.getBillNo());//商户订单号.
        paraMap.put("fee_type","CNY");//货币类型.
        paraMap.put("total_fee",request.getAmount().multiply(new BigDecimal(100)).intValue()+"");//总金额.
        paraMap.put("spbill_create_ip",request.getIp());//终端IP.
        paraMap.put("time_start", DateUtils.convert(new Date(),DateUtils.DATE_TIMESTAMP_SHORT_FORMAT));//交易起始时间.
        paraMap.put("time_expire",DateUtils.convert(new Date(System.currentTimeMillis()+5*60*1000),DateUtils.DATE_TIMESTAMP_SHORT_FORMAT));//交易结束时间.
        paraMap.put("notify_url",notify_url);//通知地址.
        paraMap.put("trade_type","MWEB");//交易类型.
        paraMap.put("limit_pay","no_credit");//指定支付方式.no_credit--指定不能使用信用卡支付
        paraMap.put("scene_info", "{\"h5_info\":{\"type\":\"Wap\",\"wap_url\":"+wap_url+",\"wap_name\": \"订单支付\"}}");//场景信息.
        paraMap.put("sign_type","MD5");//签名类型.
        String sign = null;
        String xml=null;
        try {
            //注：mch_key为商户平台设置的密钥key
            sign = this.generateSignature(paraMap,mch_key);
            paraMap.put("sign", sign);//签名.
            xml = this.mapToXml(paraMap);//将所有参数(map)转xml格式
        } catch (Exception e) {
            log.error("微信支付请求数据加密失败,billNo:{},error:{}",request.getBillNo(),e);
            transResult.setTransStatus(2);
            transResult.setMsg(ErrorMsg.DATA_ENCRYPTION_FAIL);
            return transResult;
        }

        SubjectInThirdBills subjectInThirdBills=new SubjectInThirdBills();
        subjectInThirdBills.setSubjectCode(request.getSubjectCode());
        subjectInThirdBills.setMerchantBillNo(request.getMerchantBillNo());
        subjectInThirdBills.setBillNo(request.getBillNo());
        subjectInThirdBills.setStatus(0);
        subjectInThirdBills.setAmount(request.getAmount());
        subjectInThirdBills.setRequestUrl(requestUrl);
        subjectInThirdBills.setRequestText(JSON.toJSONString(paraMap));
        subjectInThirdBills.setBeginTime(System.currentTimeMillis());
        subjectInThirdBills.setServiceType(request.getServiceType());

        String str = null;
        try {
            // 统一下单 https://api.mch.weixin.qq.com/pay/unifiedorder
            str = restTemplate.postForObject(requestUrl,xml,String.class);
        } catch (Exception e) {
            log.error("调用微信h5支付统一下单接口异常,billNo:{},error:{}",request.getBillNo(),e);
            transResult.setMsg(ErrorMsg.PAY_INTERFACE_CALL_FAIL);
        }

        subjectInThirdBills.setResponseText((StringUtils.isNotBlank(str) && str.length()>4000)?str.substring(0,4000):str);
        subjectInThirdBills.setEndTime(System.currentTimeMillis());

        if (StringUtils.isBlank(str)) {
            //默认交易处理中
            log.error("调用微信h5支付统一下单接口返回空,billNo:{}",request.getBillNo());
            transResult.setMsg(ErrorMsg.PAY_INTERFACE_RETURN_BLANK);
        }else {
            Map<String, String> resultMap=null;
            try {
                resultMap = this.xmlToMap(str);//解析微信返回的结果集
            } catch (Exception e) {
                log.error("解析微信返回数据异常,billNo:{}",request.getBillNo());
                transResult.setMsg(ErrorMsg.PAY_INTERFACE_RETURN_BLANK);
            }

            if(null!=resultMap){
                //返回状态码 此字段是通信标识，非交易标识
                String return_code = resultMap.get("return_code");
                //返回状态码
                String return_msg = resultMap.get("return_msg");
                if(StringUtils.equals("SUCCESS",return_code)){
                    //验签
                    String returnSign = resultMap.get("sign");
                    String returnSign2 = null;
                    try {
                        returnSign2 = this.generateSignature(resultMap,mch_key);
                    } catch (Exception e) {
                        log.error("微信支付请求返回数据加密失败,billNo:{},error:{}",request.getBillNo(),e);
                        //交易失败
                        transResult.setTransStatus(2);
                        subjectInThirdBills.setStatus(2);
                        transResult.setMsg(ErrorMsg.RETURN_DATA_CHECK_SIGN_FAIL);
                    }
                    //验签成功
                    if(StringUtils.equals(returnSign,returnSign2)){
                        //业务结果
                        String result_code = resultMap.get("result_code");
                        String err_code_des = resultMap.get("err_code_des");
                        if(StringUtils.equals("SUCCESS",result_code)){
                            //支付跳转链接
                            String mweb_url = resultMap.get("mweb_url");
                            //预支付交易会话标识
                            String prepay_id = resultMap.get("prepay_id");
                            transResult.setTransStatus(0);
                            transResult.setNextStepData(mweb_url);
                            transResult.setTransNo(prepay_id);
                            subjectInThirdBills.setStatus(0);
                        }else {
                            //交易失败
                            transResult.setTransStatus(2);
                            subjectInThirdBills.setStatus(2);
                            transResult.setMsg(err_code_des);
                        }
                    }else {
                        //交易失败
                        transResult.setTransStatus(2);
                        subjectInThirdBills.setStatus(2);
                        transResult.setMsg(ErrorMsg.RETURN_DATA_CHECK_SIGN_FAIL);
                    }

                }else {
                    //交易失败
                    transResult.setTransStatus(2);
                    subjectInThirdBills.setStatus(2);
                    transResult.setMsg(ErrorMsg.PAY_INTERFACE_CALL_FAIL);
                }

            }else {
                transResult.setMsg(ErrorMsg.PAY_INTERFACE_RETURN_BLANK);
            }
        }

        try {
            subjectInThirdBillsService.insert(subjectInThirdBills);
        } catch (Exception e) {
            log.error("微信h5支付插入第三方订单表异常,billNo:{},error:{}",request.getBillNo(),e);
        }

        return transResult;
    }


    /**
     * 生成签名. 注意，若含有sign_type字段，必须和signType参数保持一致。
     *
     * @param data 待签名数据
     * @param key API密钥
     * @return 签名
     */
    public String generateSignature(final Map<String, String> data, String key) throws Exception{
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals("sign")) {
                continue;
            }
            // 参数值为空，则不参与签名
            if (data.get(k).trim().length() > 0){
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
            }
        }
        sb.append("key=").append(key);
        return MD5(sb.toString()).toUpperCase();
    }

    /**
     * 生成 MD5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public String MD5(String data) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] array = md.digest(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public String generateNonceStr() {
        String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        Random RANDOM = new SecureRandom();
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }

    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public String mapToXml(Map<String, String> data) throws Exception {
        org.w3c.dom.Document document = this.newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key: data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        }
        catch (Exception ex) {
        }
        return output;
    }


    /**
     * XML格式字符串转换为Map
     *
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<String, String>();
            DocumentBuilder documentBuilder = this.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            log.warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
            throw ex;
        }

    }

    public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        documentBuilderFactory.setXIncludeAware(false);
        documentBuilderFactory.setExpandEntityReferences(false);

        return documentBuilderFactory.newDocumentBuilder();
    }

    public Document newDocument() throws ParserConfigurationException {
        return newDocumentBuilder().newDocument();
    }
}
